package pr2;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import pr2.cromosoma.Cromosoma;
import pr2.cruce.EstrategiaCruce;
import pr2.mutacion.EstrategiaMutacion;
import pr2.seleccion.EstrategiaSeleccion;

public abstract class AlgoritmoGenetico<E extends Cromosoma<E>> {

	public static final double EPSILON = 0.0001;

	private final boolean maximizar;
	protected int numElite;
	private final ArrayList<E> elite;
	protected ArrayList<E> poblacion;
	private EstrategiaCruce<E> cruce;
	private EstrategiaSeleccion<E> seleccion;
	private EstrategiaMutacion<E> mutacion;
	protected ArrayList<E> hijos;
	protected final ArrayList<E> seleccionados;
	private double probCruce;
	private double probMutacion;
	private E mejor;
	private double mejorAptitud;
	private Comparator comparator;
	private boolean escalado;
	protected int numIteracion;
    private int maxIteracionesEscaladas;

	public AlgoritmoGenetico(int tamPoblacion, int elitismo, boolean maximizar) {

		this.maximizar = maximizar;
		this.numElite = elitismo;

		if (maximizar) { // orden inverso => el primero el mayor
			comparator = new Comparator<E>() {

				public int compare(E o1, E o2) {
					return -Double.compare(o1.getAdaptacion(),
							o2.getAdaptacion());
				}

			};
			mejorAptitud = Double.MIN_VALUE;
		} else { // orden natural => el primero el menor
			comparator = new Comparator<E>() {

				public int compare(E o1, E o2) {
					return Double.compare(o1.getAdaptacion(),
							o2.getAdaptacion());
				}

			};

			mejorAptitud = Double.MAX_VALUE;
		}

		this.cruce = null;
		this.seleccion = null;

		mejor = nuevoCromosoma();

		elite = new ArrayList<E>(elitismo);
		poblacion = new ArrayList<E>(tamPoblacion + elitismo);
		seleccionados = new ArrayList<E>(tamPoblacion);
		hijos = new ArrayList<E>(tamPoblacion + elitismo);

		for (int i = 0; i < tamPoblacion; i++) {
			poblacion.add(nuevoCromosoma());
			seleccionados.add(nuevoCromosoma());
			hijos.add(nuevoCromosoma());
		}

		evalua();
	}

	protected abstract E nuevoCromosoma();

	public ArrayList<E> getPoblacion() {
		return poblacion;
	}

	public EstrategiaCruce<E> getCruce() {
		return cruce;
	}

	public void setCruce(EstrategiaCruce<E> cruce) {
		this.cruce = cruce;
	}

	public EstrategiaMutacion<E> getMutacion() {
		return mutacion;
	}

	public void setMutacion(EstrategiaMutacion<E> mutacion) {
		this.mutacion = mutacion;
	}

	public EstrategiaSeleccion<E> getSeleccion() {
		return seleccion;
	}

	public void setSeleccion(EstrategiaSeleccion<E> seleccion) {
		this.seleccion = seleccion;
	}

	public ArrayList<E> getHijos() {
		return hijos;
	}

	public ArrayList<E> getSeleccionados() {
		return seleccionados;
	}

	public double getProbCruce() {
		return probCruce;
	}

	public void setProbCruce(double probCruce) {
		this.probCruce = probCruce;
	}

	public double getProbMutacion() {
		return probMutacion;
	}

	public void setProbMutacion(double probMutacion) {
		this.probMutacion = probMutacion;
	}

	public E getMejor() {
		return mejor;
	}

	public void setMejor(E mejor) {
		this.mejor = mejor;
	}

	public E getMejorActual() {
		return poblacion.get(0);
	}

	public Comparator getComparator() {
		return comparator;
	}

	public void setComparator(Comparator comparator) {
		this.comparator = comparator;
	}

	public boolean isEscalado() {
		return escalado;
	}

	public void setEscalado(boolean escalado) {
		this.escalado = escalado;
	}

    public int getNumIteracion() {
        return numIteracion;
    }

    public void setNumIteracion(int numIteracion) {
        this.numIteracion = numIteracion;
    }

	public final void evalua() {

		E maxLocal = null;
		E minLocal = null;
		double maxAptLocal = Double.MIN_VALUE;
		double minAptLocal = Double.MAX_VALUE;
		double amplitud = 0;

        /*
         * ls adaptaciones podrian ser negativas y positivas, lo cual complica calcular la amplitud.
         * Por lo tanto lo primero que hacemos es transformar todos los valores a una escala positiva.
         * Ademas si es un minimo se cambian los signos para que el menor sea el mayor y viceversa
         */
        
        //un primer bucle para obtener los valores iniciales con sus maximos y minimos
		for (E c : poblacion) {
			double apt = c.evalua();
			c.setAdaptacion(apt);
			amplitud += apt;
			if (apt > maxAptLocal) {
				maxLocal = c;
				maxAptLocal = apt;
			}
			if (apt < minAptLocal) {
				minLocal = c;
				minAptLocal = apt;
			}
		}
        
        /*
         * Antes de cambiar estos valores para hacer las transformaciones de correccion para obtener
         * la puntuacion, calculamos si tenemos o no un cromosoma mejor que el mejor historico
         */
        if (maximizar) {
			if (maxAptLocal > mejorAptitud) {
				mejor.copia(maxLocal);
				mejorAptitud = maxAptLocal;
			}
		} else {
			if (minAptLocal < mejorAptitud) {
				mejor.copia(minLocal);
				mejorAptitud = minAptLocal;
			}
		}

		double origen = 0; //sera nuestro nuevo origen
		int signo; //-1 si tenemos que cambiar el maximo a minimo y viceversa o 1 si hay que mantener el orden
		int size = poblacion.size();
		if (maximizar) {
			origen = minAptLocal;
			amplitud -= minAptLocal * size; //corregimos la amplitud
			signo = 1;
		} else {
			origen = maxAptLocal;
			amplitud -= maxAptLocal * size; //corregimos la amplitud
            //ahora la suma es negativa (todos los valores se han hecho negativos)
			amplitud = -amplitud; //y cambiamos el signo (ahora es positiva)
			signo = -1; //cambiamos maximos por minimos
		}
		if (Math.abs(amplitud) < EPSILON) { // max y min iguales => amplitud == 0
			amplitud = maxAptLocal * size;
			origen = 0;
			signo = 1;
		}
        
        //segun la mejora usada, cambiamos las aptitudes para mejorar las puntuaciones
        aplicarMejora();
        
		double puntAcum = 0;
		for (E c : poblacion) {
			double puntuacion = signo * (c.getAdaptacion() - origen)
					/ amplitud;
            //comprobacones de correccion
			assert (puntuacion >= 0 - EPSILON / 2 && puntuacion <= 1 + EPSILON / 2) : "La puntuacion '"
					+ puntuacion + "'es invalida";
			puntAcum += puntuacion;
			c.setPuntuacion(puntuacion);
		}
		assert (puntAcum > 0.9 && puntAcum < 1.1) : "La probabilidad no suma 1 sino "
				+ puntAcum;

		Collections.sort(poblacion, comparator);
	}
    
    private void aplicarMejora() {
        if(escalado) {
            aplicarEscalado();
        }
    }

	private void aplicarEscalado() {
        
        if(numIteracion > maxIteracionesEscaladas) { //los pasos finales no escalamos para favorecer los mejores
            return ;
        }
        
		double sumaAdapt = 0;

		Collections.sort(poblacion, comparator);

		for (Cromosoma c : poblacion) {
			sumaAdapt += c.getAdaptacion();
		}

		double media = sumaAdapt / poblacion.size();
//		double a = ((p - 1) * media)
//				/ (poblacion.get(0).getAdaptacion() - media);
        double a = media / (media - poblacion.get(0).getAdaptacion());
		double b = (1 - a) * media;

		for (Cromosoma c : poblacion) {
			c.setAdaptacion(a * c.getAdaptacion() + b);
		}
	}

	public void selecciona() {
		elite.clear();
		for (int i = 0; i < numElite; i++) {
			elite.add(poblacion.get(i).clone());
		}
		seleccion.seleccion(poblacion, seleccionados, this);
	}

	public void cruza() {
		cruce.cruce(seleccionados, probCruce, hijos);
	}

	public void muta() {
		for (E c : hijos) {
			mutacion.muta(c, probMutacion);
		}
	}

	public void reemplaza() {
		ArrayList<E> aux = hijos;
		hijos = poblacion;
		poblacion = aux;

		poblacion.addAll(elite);
	}

	public void nuevaIteracion() {
		int size = poblacion.size();
        numIteracion++;
        selecciona();
		cruza();
		muta();
		reemplaza();
		evalua();
		for (int i = 0; i < numElite; i++) {
			poblacion.remove(size - i - 1);
		}
		assert (size == poblacion.size()) : poblacion.size();
	}

}
